# preprocessor.py
# -*- coding: utf-8 -*-

import re
import logging
import unicodedata # 用于 NFKC 规范化
import jieba
from typing import List, Tuple, Optional, Any
import config # 导入配置

# 配置 jieba 日志级别
jieba.setLogLevel(logging.INFO)
# 获取模块日志记录器
logger = logging.getLogger(__name__)

# --- 正则表达式常量 ---
# 水平空白符 (包括全角空格)
RE_HORIZONTAL_WHITESPACE = re.compile(r'[ \t\f\v\u3000]+')
# 非文本字符 (控制字符等，不包括换行和回车)
RE_NON_TEXT = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]')
# 多行模式下的行首空白
RE_LEADING_WHITESPACE = re.compile(r'^[ \t\f\v\u3000]+', re.MULTILINE)
# 多行模式下的行末空白
RE_TRAILING_WHITESPACE = re.compile(r'[ \t\f\v\u3000]+$', re.MULTILINE)

# 正则分句的核心表达式 (基于常见中文句末标点)
# 匹配：非句末标点 + 句末标点 + 可选的右引号/括号 + （预测积极向后查找空白或另一个句首标点）
RE_SENTENCE_BREAK = re.compile(
    # 主体：一个或多个非句末标点，后跟一个或多个句末标点
    r'([^。！？\.!?．⁈‼⁇]+[。！？\.!?．⁈‼⁇]+)'
    # 可选的结束性标点（如引号、括号）
    r'((?:[」』”"]|\)|】|\rient|\ueba0)?(?:[ \t\f\v\u3000]*))' # \rient, \ueba0 是为了处理一些特殊文档提取的字符
    # 向后查看：后面是空白、换行、文件结尾，或另一个句子的开始标志（如开引号、开括号）
    r'(?=\s|\n|$|[「『“"\(【（])'
)

def clean_text(text: str) -> str:
    """
    清理文本字符串。
    - 移除特定控制字符。
    - 规范化换行符。
    - 压缩连续空行。
    - 移除行首行尾空白。
    - 压缩连续水平空白为单个空格。
    - NFKC 规范化。
    - 移除首尾全局空白。
    """
    if not text:
        return ""
    logger.debug("开始清理文本...")
    try:
        # 1. 移除控制字符 (保留 \n, \r)
        cleaned = RE_NON_TEXT.sub('', text)
        # 2. 规范化换行符
        cleaned = cleaned.replace('\r\n', '\n').replace('\r', '\n')
        # 3. 压缩连续空行 (3个以上换行符变成2个)
        cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
        # 4. 移除每行行首和行尾的空白 (包括全角空格)
        cleaned = RE_LEADING_WHITESPACE.sub('', cleaned)
        cleaned = RE_TRAILING_WHITESPACE.sub('', cleaned)
        # 5. 将连续的水平空白符（包括全角空格）替换为单个半角空格
        cleaned = RE_HORIZONTAL_WHITESPACE.sub(' ', cleaned)
        # 6. Unicode NFKC 规范化 (例如，全角数字字母转半角)
        cleaned = unicodedata.normalize('NFKC', cleaned)
        # 7. 移除整个文本首尾的空白 (包括换行符)
        cleaned = cleaned.strip()
        
        logger.debug(f"文本清理完成。原始长度: {len(text)}, 清理后长度: {len(cleaned)}")
        return cleaned
    except Exception as e:
        logger.error(f"清理文本时发生错误: {e}", exc_info=True)
        return "" # 返回空字符串表示清理失败或原文本无效

def split_into_paragraphs_with_pos(text: str) -> List[Tuple[str, int, int]]:
    """
    将清理后的文本分割成“大段落”。
    返回段落列表，每个元素为 (段落文本, 在原text中的起始位置, 结束位置)。
    段落文本会被 strip()，位置信息对应 strip() 后的内容在原 text 中的位置。
    """
    paragraphs_with_pos: List[Tuple[str, int, int]] = []
    if not text:
        return paragraphs_with_pos

    # 从配置获取段落分割正则，若无则使用默认
    para_split_regex_str = config.PARAGRAPH_SPLIT_REGEX
    try:
        re_paragraph_sep = re.compile(para_split_regex_str)
    except re.error as e:
        logger.error(f"配置的段落分割正则表达式无效: '{para_split_regex_str}', 错误: {e}. 将使用默认 r'\\n\\n+'")
        re_paragraph_sep = re.compile(r'\n\n+')
    
    min_len = getattr(config, 'MIN_SEMANTIC_CHUNK_LENGTH', 10) # 段落也受此最小长度影响
    logger.debug(f"开始按正则 '{re_paragraph_sep.pattern}' 分割大段落 (最小有效长度={min_len})...")
    
    last_end = 0
    try:
        for match in re_paragraph_sep.finditer(text):
            para_text_raw = text[last_end : match.start()]
            para_text_stripped = para_text_raw.strip()
            
            if len(para_text_stripped) >= min_len:
                # 计算 strip 后文本在原 para_text_raw 中的偏移
                strip_start_offset = para_text_raw.find(para_text_stripped)
                # 如果 find 返回 -1 (理论上只有当 para_text_stripped 为空时，但前面已过滤长度)
                # 或者 para_text_stripped 本身就是空的 (也被长度过滤了)
                # 确保偏移有效
                if strip_start_offset == -1 and para_text_stripped: strip_start_offset = 0 
                
                actual_start = last_end + strip_start_offset
                actual_end = actual_start + len(para_text_stripped)
                paragraphs_with_pos.append((para_text_stripped, actual_start, actual_end))
                logger.debug(f"  找到段落: 位置({actual_start}-{actual_end}), 长度={len(para_text_stripped)}, 内容='{para_text_stripped[:50]}...'")
            elif para_text_stripped: # 记录被过滤的非空短段落
                 logger.debug(f"  过滤短段落: 长度={len(para_text_stripped)} (< {min_len}), 内容='{para_text_stripped[:50]}...'")
            
            last_end = match.end() # 下一段从分隔符之后开始

        # 处理最后一个分隔符之后剩余的文本作为最后一段
        final_para_text_raw = text[last_end:]
        final_para_text_stripped = final_para_text_raw.strip()
        if len(final_para_text_stripped) >= min_len:
            strip_start_offset = final_para_text_raw.find(final_para_text_stripped)
            if strip_start_offset == -1 and final_para_text_stripped: strip_start_offset = 0
            
            actual_start = last_end + strip_start_offset
            actual_end = actual_start + len(final_para_text_stripped)
            paragraphs_with_pos.append((final_para_text_stripped, actual_start, actual_end))
            logger.debug(f"  找到末尾段落: 位置({actual_start}-{actual_end}), 长度={len(final_para_text_stripped)}, 内容='{final_para_text_stripped[:50]}...'")
        elif final_para_text_stripped:
             logger.debug(f"  过滤末尾短段落: 长度={len(final_para_text_stripped)} (< {min_len}), 内容='{final_para_text_stripped[:50]}...'")

    except Exception as e:
        logger.error(f"分割大段落时发生错误: {e}", exc_info=True)
        return [] # 出错时返回空列表

    logger.info(f"大段落分割完成。共找到 {len(paragraphs_with_pos)} 个有效段落。")
    return paragraphs_with_pos

def _split_long_sentence(sentence_text: str, global_start_offset: int, max_len: int, min_len: int) -> List[Tuple[str, int, int]]:
    """
    (辅助函数) 将单个长句子切分成满足长度约束的子句。
    尝试在弱分隔符处切分。
    """
    sub_sentences_with_pos: List[Tuple[str, int, int]] = []
    current_sub_start_in_sentence = 0 # 子句在原长句子中的相对起始位置

    while current_sub_start_in_sentence < len(sentence_text):
        # 确定当前子句的理想结束位置 (不超过 max_len)
        potential_end_in_sentence = min(current_sub_start_in_sentence + max_len, len(sentence_text))
        
        # 从 potential_end_in_sentence 向前查找弱分隔符 (在当前子句范围内)
        # 切分点应该在 (current_sub_start_in_sentence, potential_end_in_sentence] 区间内
        actual_split_pos_in_sentence = potential_end_in_sentence
        
        # 只有当子句长度确实超过 max_len 时，才尝试寻找弱分隔符
        if potential_end_in_sentence - current_sub_start_in_sentence >= max_len and potential_end_in_sentence < len(sentence_text):
            # 搜索范围是 [current_sub_start_in_sentence, potential_end_in_sentence -1]
            search_start = current_sub_start_in_sentence + 1 # 避免切出空串
            search_end = potential_end_in_sentence
            
            best_weak_split = -1
            for match in config.RE_WEAK_DELIMITERS.finditer(sentence_text, search_start, search_end):
                # match.end() 是分隔符后的位置，我们在这里切
                if match.end() > best_weak_split: # 找最靠后的一个弱分隔符
                    best_weak_split = match.end()
            
            if best_weak_split != -1: # 找到了弱分隔符
                 # 确保切分后的第一部分不太短 (例如，至少 min_len/2，或者不小于一定字符)
                 if best_weak_split - current_sub_start_in_sentence >= min_len / 2 and best_weak_split - current_sub_start_in_sentence > 3:
                    actual_split_pos_in_sentence = best_weak_split
                 else:
                    logger.debug(f"  长句切分：找到弱分隔符于 {best_weak_split}，但切分后前半段过短，将尝试硬切分或按 max_len 切。")
                    # 如果弱分隔符切出来太短，还是按 potential_end_in_sentence (max_len) 来
                    actual_split_pos_in_sentence = potential_end_in_sentence
            else: # 未找到合适的弱分隔符，按 max_len 切
                actual_split_pos_in_sentence = potential_end_in_sentence
                logger.debug(f"  长句切分：在'{sentence_text[current_sub_start_in_sentence:potential_end_in_sentence]}' 中未找到弱分隔符，按 max_len ({max_len}) 切分。")

        # 提取子句文本 (raw)
        sub_sentence_raw = sentence_text[current_sub_start_in_sentence : actual_split_pos_in_sentence]
        sub_sentence_stripped = sub_sentence_raw.strip()
        
        if len(sub_sentence_stripped) >= min_len:
            strip_offset = sub_sentence_raw.find(sub_sentence_stripped)
            if strip_offset == -1 and sub_sentence_stripped: strip_offset = 0
            
            sub_global_start = global_start_offset + current_sub_start_in_sentence + strip_offset
            sub_global_end = sub_global_start + len(sub_sentence_stripped)
            sub_sentences_with_pos.append((sub_sentence_stripped, sub_global_start, sub_global_end))
            logger.debug(f"    长句切分出子句: 位置({sub_global_start}-{sub_global_end}), 内容='{sub_sentence_stripped[:50]}...'")
        elif sub_sentence_stripped:
            logger.debug(f"    长句切分过滤短子句: 长度={len(sub_sentence_stripped)}, 内容='{sub_sentence_stripped[:50]}...'")
            
        current_sub_start_in_sentence = actual_split_pos_in_sentence # 更新下一子句的起始点

    return sub_sentences_with_pos


def split_into_sentences_with_pos(text: str, paragraphs_for_lookup: Optional[List[Tuple[str, int, int]]] = None) -> List[Tuple[str, int, int]]:
    """
    将文本分割成句子。
    - 首先，如果未提供段落，则调用 split_into_paragraphs_with_pos 获取“大段落”。
    - 然后，对每个大段落应用基于正则表达式的分句 (RE_SENTENCE_BREAK)。
    - 对分出的句子进行长度过滤 (MIN_SEMANTIC_CHUNK_LENGTH)。
    - 对过长的句子 (MAX_SEMANTIC_CHUNK_LENGTH) 进行切分，尝试在弱分隔符处切。
    - 返回所有有效句子的列表，格式为 (句子文本, 全局起始位置, 全局结束位置)。
    - 结果按全局起始位置排序。
    """
    sentences_with_pos: List[Tuple[str, int, int]] = []
    if not text:
        return sentences_with_pos

    min_len = config.MIN_SEMANTIC_CHUNK_LENGTH
    max_len = config.MAX_SEMANTIC_CHUNK_LENGTH
    splitter_type = config.SENTENCE_SPLITTER_TYPE
    logger.debug(f"开始进行分句 (方式: {splitter_type}, 最小长度={min_len}, 最大长度={max_len})...")

    if paragraphs_for_lookup is None:
        logger.debug("未提供段落信息，将首先进行大段落分割...")
        paragraphs_for_lookup = split_into_paragraphs_with_pos(text)
        if not paragraphs_for_lookup: # 如果没有有效的大段落
            logger.warning("未能分割出任何有效的大段落，无法进行分句。")
            return sentences_with_pos
    
    if splitter_type == 'regex':
        for para_text, para_start_global, _ in paragraphs_for_lookup: # para_end_global 未使用
            if not para_text: # 跳过空段落
                continue
            
            logger.debug(f"  处理段落 (始于 {para_start_global}): '{para_text[:80]}...'")
            current_sentence_start_in_para = 0 # 当前句子在段落内的相对起始位置
            
            # 使用正则在当前段落内查找句子边界
            for match in RE_SENTENCE_BREAK.finditer(para_text):
                # 提取从上个句末到当前匹配句末的文本作为候选句子
                sentence_text_raw_in_para = para_text[current_sentence_start_in_para : match.end()]
                # 记录这个句子在段落内的原始起止（相对位置）
                sentence_start_in_para_raw = current_sentence_start_in_para
                # 更新下一个句子的起始点
                current_sentence_start_in_para = match.end()
                
                sentence_text_stripped = sentence_text_raw_in_para.strip()
                sentence_len = len(sentence_text_stripped)

                if sentence_len >= min_len:
                    # 计算 strip 后的文本在 raw 文本中的偏移
                    strip_offset_in_raw = sentence_text_raw_in_para.find(sentence_text_stripped)
                    if strip_offset_in_raw == -1 and sentence_text_stripped: strip_offset_in_raw = 0
                    
                    # 计算句子的全局起始位置
                    actual_global_start = para_start_global + sentence_start_in_para_raw + strip_offset_in_raw
                    
                    if sentence_len > max_len:
                        logger.debug(f"    句子过长 (长度 {sentence_len} > {max_len})，将进行切分: '{sentence_text_stripped[:50]}...'")
                        sub_sentences = _split_long_sentence(sentence_text_stripped, actual_global_start, max_len, min_len)
                        sentences_with_pos.extend(sub_sentences)
                    else:
                        actual_global_end = actual_global_start + sentence_len
                        sentences_with_pos.append((sentence_text_stripped, actual_global_start, actual_global_end))
                        logger.debug(f"    找到句子: 位置({actual_global_start}-{actual_global_end}), 内容='{sentence_text_stripped[:50]}...'")
                elif sentence_text_stripped: # 记录被过滤的非空短句
                    logger.debug(f"    过滤短句子: 长度={sentence_len} (< {min_len}), 内容='{sentence_text_stripped[:50]}...'")

            # 处理段落末尾可能剩余的文本 (未被 RE_SENTENCE_BREAK 匹配到的最后部分)
            remaining_text_raw_in_para = para_text[current_sentence_start_in_para:]
            remaining_text_stripped = remaining_text_raw_in_para.strip()
            remaining_len = len(remaining_text_stripped)
            
            if remaining_len >= min_len:
                strip_offset_in_raw = remaining_text_raw_in_para.find(remaining_text_stripped)
                if strip_offset_in_raw == -1 and remaining_text_stripped: strip_offset_in_raw = 0
                
                actual_global_start = para_start_global + current_sentence_start_in_para + strip_offset_in_raw
                
                if remaining_len > max_len:
                    logger.debug(f"    段末剩余句子过长 (长度 {remaining_len} > {max_len})，将进行切分: '{remaining_text_stripped[:50]}...'")
                    sub_sentences = _split_long_sentence(remaining_text_stripped, actual_global_start, max_len, min_len)
                    sentences_with_pos.extend(sub_sentences)
                else:
                    actual_global_end = actual_global_start + remaining_len
                    sentences_with_pos.append((remaining_text_stripped, actual_global_start, actual_global_end))
                    logger.debug(f"    找到段末剩余句子: 位置({actual_global_start}-{actual_global_end}), 内容='{remaining_text_stripped[:50]}...'")
            elif remaining_text_stripped:
                 logger.debug(f"    过滤段末剩余短句子: 长度={remaining_len} (< {min_len}), 内容='{remaining_text_stripped[:50]}...'")
    
    elif splitter_type == 'ltp': # 或其他模型
        logger.warning(f"分句方式配置为 '{splitter_type}'，但当前版本 preprocessor.py 尚未完全实现此功能。将回退到不分句（或返回空）。")
        # TODO: 在此实现调用外部模型分句的逻辑
        pass # 暂不处理

    else:
        logger.error(f"未知的分句器类型: '{splitter_type}'。无法进行分句。")

    # 确保最终结果按起始位置排序 (理论上按段处理已经是顺序的，但长句切分可能打乱)
    sentences_with_pos.sort(key=lambda x: x[1])
    
    logger.info(f"分句处理完成 (方式: {splitter_type})。共找到 {len(sentences_with_pos)} 个有效句子。")
    return sentences_with_pos


def tokenize(text: str, filter_single_char_alnum: bool = True) -> List[str]:
    """
    使用 jieba 进行中文分词。
    过滤掉无效 token (空、纯标点/空白等)，并对结果进行 strip()。
    根据参数决定是否过滤单字母和单数字。默认过滤。
    """
    if not text:
        return []

    def contains_meaningful_char(token: str) -> bool:
        # 检查 token 是否包含至少一个字母、数字或汉字
        if not token: return False
        return bool(re.search(r'[a-zA-Z0-9\u4e00-\u9fff]', token))

    raw_tokens: List[str] = []
    logger.debug("开始使用 jieba 进行分词...")
    try:
        # 使用精确模式，关闭HMM（HMM对新词发现有帮助，但可能降低已知词的准确性，看需求）
        raw_tokens = jieba.lcut(text, cut_all=False, HMM=True) 
        logger.debug(f"jieba 原始分词数: {len(raw_tokens)}")
    except Exception as e:
        logger.error(f"jieba 分词过程中发生错误: {e}", exc_info=True)
        # 回退策略：按空格分割 (对于纯英文或已用空格分词的文本有一定作用)
        logger.warning("jieba 分词失败，将回退到按空格分割。")
        raw_tokens = re.split(r'\s+', text.strip())
        
    filtered_tokens: List[str] = []
    for token in raw_tokens:
        if not token: continue # 过滤空 token
        
        stripped_token = token.strip() # 去除 token自身的前后空白
        if not stripped_token: continue

        # 过滤纯标点、纯空白或不包含有意义字符的 token
        if not contains_meaningful_char(stripped_token):
            logger.debug(f"  过滤无效字符 token: '{stripped_token}'")
            continue
            
        # 过滤单字符（可选，对于英文 'a', 'I' 或数字可能需要保留）
        if len(stripped_token) == 1:
            # 如果是字母或数字，且配置了不过滤单字母数字，则保留
            if stripped_token.isalnum() and not filter_single_char_alnum:
                pass # 保留
            # 如果是汉字，通常单汉字意义不大，除非是特定场景（如古文、单字关键词）
            # 此处简单处理：如果不是字母数字的单字符，通常是标点或特殊符号，上面已过滤
            # 如果是单汉字且 filter_single_char_alnum=True (默认)，这里会被过滤
            elif stripped_token.isalnum() and filter_single_char_alnum: # 明确过滤单字母数字
                 logger.debug(f"  过滤单字母/数字 token: '{stripped_token}' (因 filter_single_char_alnum=True)")
                 continue
            elif not stripped_token.isalnum(): # 非字母数字的单字符，之前 contains_meaningful_char 应该已经处理了
                 logger.debug(f"  过滤单非字母数字 token: '{stripped_token}'")
                 continue


        filtered_tokens.append(stripped_token)
    
    logger.debug(f"jieba 分词后，过滤得到 {len(filtered_tokens)} 个有效 token。")
    return filtered_tokens


def preprocess_text(raw_text: str) -> Tuple[str, List[Tuple[str, int, int]], List[Tuple[str, int, int]], int, int]:
    """
    执行完整的文本预处理流程。
    1. 清洗文本。
    2. 分割成大段落。
    3. (基于大段落) 分割成句子 (当前使用优化后的正则)。
    4. (对清洗后全文) 进行分词并统计词数。
    """
    logger.info("开始执行文本预处理流程...")
    
    if not raw_text:
        logger.warning("输入原始文本为空，预处理中止。")
        return "", [], [], 0, 0

    # 1. 清洗文本
    logger.debug("步骤 1: 清洗文本...")
    cleaned_text = clean_text(raw_text)
    if not cleaned_text:
        logger.warning("文本清洗后内容为空。")
        return "", [], [], 0, 0
    logger.debug(f"文本清洗完成。")

    # 2. 分割成大段落
    logger.debug("步骤 2: 分割大段落...")
    paragraphs_with_pos = split_into_paragraphs_with_pos(cleaned_text)
    total_paragraphs_count = len(paragraphs_with_pos)
    logger.debug(f"分割得到 {total_paragraphs_count} 个有效大段落。")

    # 3. 分割成句子 (基于大段落)
    logger.debug("步骤 3: 分割句子...")
    # 此处传入 paragraphs_with_pos 可以避免 split_into_sentences_with_pos 内部重复计算
    sentences_with_pos = split_into_sentences_with_pos(cleaned_text, paragraphs_for_lookup=paragraphs_with_pos)
    logger.debug(f"分割得到 {len(sentences_with_pos)} 个有效句子。")
    
    # 4. 分词统计 (对完整的 cleaned_text)
    logger.debug("步骤 4: 分词并统计词数...")
    # 从 config 获取是否过滤单字母数字的配置，默认为 True
    filter_single = getattr(config, 'TOKENIZER_FILTER_SINGLE_CHAR_ALPHANUMERIC', True)
    try:
        all_tokens = tokenize(cleaned_text, filter_single_char_alnum=filter_single)
        total_words = len(all_tokens)
        logger.debug(f"分词完成。有效总词数: {total_words}")
    except Exception as e:
        logger.error(f"分词统计过程中发生错误: {e}", exc_info=True)
        total_words = -1 # 表示错误
        
    logger.info(f"文本预处理流程完成。有效大段落数: {total_paragraphs_count}, 有效句子数: {len(sentences_with_pos)}, 有效总词数: {total_words}")
    return cleaned_text, paragraphs_with_pos, sentences_with_pos, total_words, total_paragraphs_count
